<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>
        .sullivan-grid {
            margin-left: 100px;
            margin-top: 100px;
            width: 600px;
            height: 600px;
            border: 1px solid red;
            display: grid;
            gap: 6px;
            user-select: none;
            .sullivan-grid-item {
                position: relative;
                border: 1px solid blue;
            }
            .sullivan-grid-item-content {
                position: absolute;
                width: 100%;
                height: 100%;
                border: 1px solid blue;
            }
        }
        .sullivan-grid-item-direction {
            background: rgba(0, 0, 0, .3)
        }
        .sullivan-grid-item-top,
        .sullivan-grid-item-bottom {
            position: absolute;
            height: 10px;
            width: 100%;
            cursor: row-resize;
        }
        .sullivan-grid-item-right,
        .sullivan-grid-item-left {
            position: absolute;
            width: 10px;
            height: 100%;
            cursor: col-resize;
        }
        .sullivan-grid-item-top {
            top: -3px;
        }
        .sullivan-grid-item-right {
            right: -3px;
        }
        .sullivan-grid-item-bottom {
            bottom: -3px;
        }
        .sullivan-grid-item-left {
            left: -3px;
        }
    </style>
</head>
<body>
<div id="container"></div>
<script src="./jquery.js"></script>
<script>
    class Grid {
        $grid;
        row;
        col;
        areas;
        areasCopy;
        areasHides = new Set();
        constructor([row, col] = [1, 1]) {
            const className = `sullivan-grid sullivan-grid-${row}x${col}`;
            this.$grid = $(`<div class="${className}" style="display: grid;" ></div>`);
            this.row = row;
            this.col = col;
        }

        createGridArea () {
            const { row, col } = this;
            const areas = [];
            for (let i = 1; i <= row * col; i++) {
                const index = i < 10 ? `0${i}` : i
                areas.push(`grid-area-${ index }`)
            }
            this.areas = areas
            this.areasCopy = [...areas]
            return areas;
        }

        update (index, area) {
            this.areasCopy[index] = area;
            if (this.areasCopy.includes(area)) {
                this.areasHides.add(area)
            } else {
                this.areasHides.delete(area)
            }
            this.setGridArea()
        }

        createGrid(grid = [1, 1], gap = [6, 6]) {
            const { row, col } = this;
            const [rowGap, columnGap] = gap;
            const rowStyle = `repeat(${row}, 1fr)`;
            const colStyle = `repeat(${col}, 1fr)`;

            this.$grid.css({
                "grid-template-rows": rowStyle,
                "grid-template-columns": colStyle,
                "grid-row-gap": rowGap,
                "grid-column-gap": columnGap,
            });

            const areas = this.createGridArea(grid);
            const createDirection = (direction) => (
                `<div class="sullivan-grid-item-direction sullivan-grid-item-${direction}"></div>`
            );

            areas.forEach((area, i) => {
                const $gridItem = $(
                    `<div class="sullivan-grid-item ${area}" data-index="${i + 1}" data-area="${area}" style="grid-area:
                        ${area}"><div class="sullivan-grid-item-content"></div></div>`
                );
                $gridItem.find('.sullivan-grid-item-content').append(['top', 'right', 'bottom', 'left'].map(createDirection))
                this.$grid.append($gridItem);
            })

            this.setGridArea()
            return this.$grid;
        }

        setGridArea () {
            let tempAreas = ``

            this.areasCopy.forEach((item, index) => {
                if (index % this.row || index === 0) {
                    tempAreas += `${item} `
                } else {
                    tempAreas += `" "${item} `
                }
            })

            this.$grid.css({
                "grid-template-areas": `"${tempAreas}"`
            });
        }
    }

    const grid = new Grid([4, 4])

    const $grid = grid.createGrid()
    const $container = $('#container');

    $container.html($grid)

    class Coordinate {
        x = 0;
        y = 0;

        constructor(x, y) {
            this.x = x;
            this.y = y;
        }

        update (x, y) {
            this.x = x;
            this.y = y;
        }
    }

    class Size {
        top;
        right;
        bottom;
        left;
        initW;
        initH;
        width;
        height;
        constructor($item) {
            this.initW = $item.attr('data-w');
            this.initH = $item.attr('data-h');

            this.width = $item.width()
            this.height = $item.height()

            console.log($item.attr('data-w'), $item.attr('data-h'))
            if (!this.initW) {
                this.initW = +this.width
                $item.attr('data-w', this.initW)
            }
            if (!this.initH) {
                this.initH = +this.height
                $item.attr('data-h', this.initH)
            }

            console.log(this.width, this.height, this.initW, this.initH)
            this.update($item)
        }

        update = ($item) => {
            const { top, left } = $item.offset();
            this.top = top;
            this.right = left + this.width;
            this.bottom = top + this.height;
            this.left = left;
        }
    }

    const activatedMap = new Map();
    class Position {
        type;
        index;
        // 设置当前操作覆盖的区域
        // activated = new Set();
        $target;
        playground;
        size;
        start;
        end;
        move;

        constructor(type) {
            this.type = type;
        }

        setStart(event) {
            const $item = $(event.target).closest('.sullivan-grid-item');
            // const activated = $item.attr('data-activated');
            // 下标
            this.index = $item.index();
            // 目标元素
            this.$item = $item;
            // 目标元素主体
            this.$target = $item.find('.sullivan-grid-item-content');
            // 开始位置
            this.start = new Coordinate(event.pageX, event.pageY);
            // 移动距离
            this.move = new Coordinate(0, 0);
            // 大小
            this.size = new Size(this.$target);
            // 背景板
            this.playground = new Size($(event.target).closest('.sullivan-grid'));

            /*this.activated.add(this.index)
            if (typeof activated === 'number') {
                this.activated.add(activated)
            } else {
                activated?.split(',')?.forEach(act => this.activated.add(+act))
            }*/

            // console.log('start: => ', event);
        }

        setMove(event) {
            const { left, right, top, bottom } = this.playground
            /**
             * 第一层限制，必须在九宫格范围内。
             */
            if (event.pageX >= left && event.pageX <= right && event.pageY >= top && event.pageY <= bottom) {
                const { left, right, top, bottom } = this.size
                // console.log(this.$target.height(), this.size.initH, this.$target.height() >= this.size.initH)
                /**
                 * 第二层限制，必须大于子区域大小。
                 */
                if (this.$target.height() >= this.size.initH || this.$target.width() >= this.size.initW) {
                    this.move.update(event.pageX - this.start.x, event.pageY - this.start.y);
                    this.updateView(event)
                }
            }
            // console.log('move: => ', this.$target.height(), this.size.initH)
        }

        setEnd(event) {
            this.end = new Coordinate(event.pageX, event.pageY);
            this.setMove(event);
            this.updateView();
            this.updateLayout();
            this.size.update(this.$target);
            console.log('end: => ', this)
        }

        /**
         * 拖动时，更新视图。
         */
        updateView () {
            const { width, height } = this.size
            const { x: left, y: top } = this.move
            switch (this.type) {
                case 'top': {
                    return this.$target.css({ height: height - top, top })
                }
                case 'right': {
                    return  this.$target.css({ width: width + left })
                }
                case 'bottom': {
                    return this.$target.css({ height: height + top })
                }
                case 'left': {
                    return this.$target.css({ width: width - left, left })
                }
            }
        }

        /**
         * 吞并周边，放大
         *
         * @param { string } area 区域 key 值
         * @param { number } moveDistance 移动距离
         * @param { number } initSize 基础值，大于基础之，则会递归添加下一个
         * @param { number[] } activated 下标集合
         * @param { number } gap 下一个间隔，距离下一个下标的间隔
         * @param { string[] } hide 即将被隐藏的区域集合
         */
        updateGridAreas (area, moveDistance, initSize, activated, gap, hide = new Set()) {
            const __activated = activated.map(act => {
                const idx = act + gap
                if (!activated.includes(idx)) {
                    hide.add(grid.areasCopy[idx])
                    grid.update(idx, area)
                }
                return idx
            })

            console.log('updateGridAreas: => ', __activated)
            // 移动距离 > 基础值，递归添加下一个
            if (moveDistance > initSize) {
                this.updateGridAreas(area, moveDistance - initSize, initSize, __activated, gap, hide)
            }
            return hide
        }

        /**
         * 抛弃周边，缩小
         *
         * @param { number } moveDistance 移动距离
         * @param { number } initSize 基础值
         * @param { number[] } activated 下标集合
         * @param { number } gap 下一个间隔，距离下一个下标的间隔
         * @param { string[] } show 即将被显示的区域集合
         */
        removeGridAreas (moveDistance, initSize, activated, gap, show = new Set()) {
            const dataIndex = this.$item.attr('data-index')
            const acts = []
            let __activated
            let prev = activated.shift();
            acts.push(prev)
            __activated = activated.filter(item => {
                if (item === prev + gap) {
                    acts.push(item)
                    prev = item
                } else {
                    return item;
                }
            })

            // console.log('removeGridAreas: => ', acts, __activated)
            // 暂时不允许自身初始位置。
            if (acts.includes(dataIndex - 1)) return show;
            acts.forEach(index => {
                const area = grid.areas[index]
                show.add(area)
                grid.update(index, area)
            })

            if (moveDistance > initSize) {
                // 移动距离 > 基础值，递归删除下一个
                this.removeGridAreas(moveDistance - initSize, initSize, __activated, gap, show)
            }
            return show
        }

        updateGridItem (hideAreas, exclude, hide = new Set()) {
            let length = hideAreas.length;
            let areaItem = hideAreas.pop();

            while (length--) {
                hide.add(grid.areasCopy.pop());
            }

            console.log('hide: => ', hide)
            while (areaItem) {
                grid.areasCopy = grid.areasCopy.reduce((def, next) => {
                    if (next !== exclude && next > areaItem) {
                        def.push(areaItem);
                        areaItem = hideAreas.pop();
                    }
                    def.push(next)
                    return def
                }, [])
            }

            return hide
        }

        /**
         * 移动被隐藏的播放器，重新排序
         */
        moveHidePlayers (hidePlayers) {
            // 找到所有被隐藏的视频
            // const hidePlayers = $(`.sullivan-grid-item:has(.base-camera-container):not(:visible)`);
            let jump = hidePlayers.length;
            let $play = hidePlayers.eq(jump - 1);
            const itemArea = $play.attr('data-area');
            // 所有拥有视频的单元格
            const $Items = [...$(`.sullivan-grid-item:has(.base-camera-container)`)];

            while ($Items.length) {
                const $item = $($Items.pop());
                const index = $item.index();
                const $next = $(`.sullivan-grid-item`).eq(index + jump);

                if ($next) {
                    const $content = $next.find('.sullivan-grid-item-content');
                    $item.find('.base-camera-container').appendTo($content);
                } else {
                    $item.find('.base-camera-container').remove();
                }




                const $player = $playerItems.eq(index).find('.base-camera-container');

                const $content = $gridItem.eq(index + jump);

                if ($content.length) {
                    $content.find('.base-camera-container').hide();
                    $content.append($player)

                    if ($item.hasClass(itemArea)) {
                        jump--
                    }
                }

                index--
            }



            // 所有单元格
            const $gridItem = $(`.sullivan-grid-item`);
            let index = $gridItem.length;
            while (index > 0) {
                const $item = $gridItem.eq(index);
                const $player = $playerItems.eq(index).find('.base-camera-container');

                const $content = $gridItem.eq(index + jump);

                if ($content.length) {
                    $content.find('.base-camera-container').hide();
                    $content.append($player)

                    if ($item.hasClass(itemArea)) {
                        jump--
                    }
                }

                index--
            }
        }

        /**
         * 隐藏周边，配合 updateGridAreas 使用
         * 将 grid.areasCopy 中不存在的隐藏掉
         *
         * @param hideAreas
         * @param activated
         * @param hides 本次实际隐藏掉的播放器
         * @returns {Set<any>}
         */
        hideGridItem (hideAreas, activated, hides = new Set()) {
            const activeAreas = activatedMap.get(activated) || new Set()
            hideAreas.forEach(area => {
                activeAreas.add(area)
                if (!grid.areasCopy.includes(area)) {
                    const $item = grid.$grid.find(`.${area}`);
                    $item.hide()
                    if ($item.find('.base-camera-container').length) {
                        hides.add($item)
                    }
                }
            })

            activatedMap.set(activated, activeAreas);
            return hides;
        }

        /**
         * 显示周边，配合 removeGridAreas 使用
         * 将已隐藏，grid.areasCopy 中重新添加的，显示出来
         *
         * @param areas
         */
        showGridItem (areas) {
            areas.forEach(area => {
                grid.$grid.find(`.${area}`).show()
            })
        }

        /**
         * 寻找到当前选中项的所有下标位置
         *
         * @param originArea
         * @returns {*[]}
         */
        findActivated (originArea) {
            console.log('findActivated: => ', originArea)
            const areas = []
            grid.areasCopy.forEach((area, index) => {
                if (area === originArea) {
                    areas.push(index)
                }
            })
            return areas;
        }

        /**
         * 更新布局
         */
        updateLayout () {
            const area = this.$item.attr('data-area')
            const { initH, initW } = this.size
            const activated = this.findActivated(area)
            switch (this.type) {
                case 'top': {
                    if (this.move.y < -20) {
                        // TODO-1 重新排列 grid-area
                        const hideAreas = this.updateGridAreas(area, -this.move.y, initH, activated, -grid.row);
                        // TODO-2 隐藏被删除等待 grid-area
                        const hidePlayers = this.hideGridItem(hideAreas, activated);
                        // TODO-3 移动被隐藏的播放器，调整布局。
                        if (hidePlayers.size) this.moveHidePlayers(hidePlayers)
                        console.log('data-activated:enlarge => ', activated, hideAreas)
                    } else if (this.move.y > 20) {
                        const showAreas = this.removeGridAreas(this.move.y, initH, activated, 1)
                        this.showGridItem(showAreas)
                        console.log('data-activated:shrink => ', activated, showAreas)
                    }
                    break
                }
                case 'right': {
                    if (this.move.x < -20) {
                        const showAreas = this.removeGridAreas(this.move.x, initW, activated.reverse(), -grid.col)
                        this.showGridItem(showAreas)
                        console.log('data-activated:shrink => ', activated, showAreas)
                    } else if (this.move.x > 20) {
                        const hideAreas = this.updateGridAreas(area, this.move.x, initW, activated, 1)
                        this.hideGridItem(hideAreas, 1)
                        console.log('data-activated:enlarge => ', activated, hideAreas)
                    }
                    break
                }
                case 'bottom': {
                    if (this.move.y < -20) {
                        const showAreas = this.removeGridAreas(this.move.y, initH, activated.reverse(), -1)
                        this.showGridItem(showAreas)
                        console.log('data-activated:shrink => ', activated, showAreas)
                    } else if (this.move.y > 20) {
                        const hideAreas = this.updateGridAreas(area, this.move.y, initH, activated, grid.row)
                        this.hideGridItem(hideAreas)
                        console.log('data-activated:enlarge => ', activated, hideAreas)
                    }
                    break
                }
                case 'left': {
                    if (this.move.x < -20) {
                        const hideAreas = this.updateGridAreas(area, -this.move.x, initW, activated, -1)
                        this.hideGridItem(hideAreas)
                        console.log('data-activated:enlarge => ', activated, hideAreas)
                    } else if (this.move.x > 20) {
                        const showAreas = this.removeGridAreas(this.move.x, initW, activated, grid.col)
                        this.showGridItem(showAreas, -1)
                        console.log('data-activated:shrink => ', activated, showAreas)
                    }
                    break
                }
            }

            this.$target.removeAttr('style')
            // this.$item.attr('data-activated', [...this.activated.values()])
        }
    }

    let position;


    const cursor = {
        set (cursor) {
            $(document.body).css('cursor', cursor + '-resize')
        },
        remove () {
            $(document.body).css('cursor', 'initial')
        }
    }

    function onTopMouseDown (event) {
        event.stopPropagation()
        cursor.set('row')
        position = new Position('top')
        position.setStart(event)
        bindMouseMove()
    }
    function onRightMouseDown (event) {
        event.stopPropagation()
        cursor.set('column')
        position = new Position('right')
        position.setStart(event)
        bindMouseMove()
    }
    function onBottomMouseDown (event) {
        event.stopPropagation()
        cursor.set('row')
        position = new Position('bottom')
        position.setStart(event)
        bindMouseMove()
    }

    function onLeftMouseDown (event) {
        event.stopPropagation()
        cursor.set('column')
        position = new Position('left')
        position.setStart(event)
        bindMouseMove()
    }

    // $container
    //     .off('mouseover', '.sullivan-grid-item', onMouseOver)
    //     .on('mouseover', '.sullivan-grid-item', onMouseOver)
    // $container
    //     .off('mouseout', '.sullivan-grid-item', onMouseOut)
    //     .on('mouseout', '.sullivan-grid-item', onMouseOut)
    function bindMouseMove () {
        $(document)
            .off('mousemove', onMouseMove)
            .on('mousemove', onMouseMove)
        $(document)
            .off('mouseup', onMouseUp)
            .on('mouseup', onMouseUp)
    }

    function offMouseMove () {
        // console.log('offMouseMove')
        cursor.remove()
        $container
            .off('mouseover', onMouseOver)
        $container
            .off('mouseout', onMouseOut)
        $(document)
            .off('mousemove', onMouseMove)
        $(document)
            .off('mouseup', onMouseUp)
    }

    function onMouseMove (event) {
        // console.log('onMouseMove')
        if (event.which) {
            position.setMove(event)
        } else {
            offMouseMove()
        }
    }

    function onMouseUp (event) {
        position?.setEnd(event);
        offMouseMove()
    }

    function onMouseOver (event) {
        // console.log('----------------onMouseOver--------------------', $(event.target).closest('.sullivan-grid-item').index())
        if (position) {
            position.activated.add($(event.target).closest('.sullivan-grid-item').index());
        }
    }

    function onMouseOut (event) {
        // console.log('----------------onMouseOut--------------------', $(event.target).closest('.sullivan-grid-item').index())
        if (position) {
            const index = $(event.target).closest('.sullivan-grid-item').index();
            if (index !== position.index) {
                position.activated.delete(index);
            }
        }
    }

    $container.on('mousedown', '.sullivan-grid-item-top', onTopMouseDown)
    $container.on('mousedown', '.sullivan-grid-item-right', onRightMouseDown)
    $container.on('mousedown', '.sullivan-grid-item-bottom', onBottomMouseDown)
    $container.on('mousedown', '.sullivan-grid-item-left', onLeftMouseDown)

</script>
</body>
</html>